﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Configuration;
using System.IO;
using System.Reflection;
using System.Text;

using LinqToCodedom;
using LinqToCodedom.Extensions;
using LinqToCodedom.Generator;

namespace NConfigGenerator
{
    public static class NewGenerator
    {
        public static string Generate(ConfigTree tree, GeneratorOptions options)
        {
            if (options == null)
            {
                options = new GeneratorOptions {
                    Language = "cs"
                };
            }

            var gen = new CodeDomGenerator();

            CodeNamespace ns = gen.AddNamespace(options.Namespace);

            CodeTypeDeclaration sectionClass = CreateElementClass(tree.RootNode, typeof(ConfigurationSection), "Section");
            CodeTypeDeclaration configClass = CreateConfigClass(tree.RootNode, sectionClass, options);
 
            ns.Types.Add(configClass);
            ns.Types.Add(sectionClass);

            return gen.GenerateCode(CodeDomGenerator.Language.CSharp);
            //var sb = new StringBuilder();
            //using (var writer = new StringWriter(sb))
            //{
            //    CodeDomProvider provider = CodeDomProvider.CreateProvider(options.Language);
            //    ICodeGenerator codeGenerator = provider.CreateGenerator(writer);
            //    var cgo = new CodeGeneratorOptions();
            //    codeGenerator.GenerateCodeFromCompileUnit(ccu, writer, cgo);
            //    writer.Flush();
            //}

            //return sb.ToString();
        }

        private static CodeTypeDeclaration CreateConfigClass(ConfigNode configNode, CodeTypeDeclaration sectionClass,
            GeneratorOptions options)
        {
            CodeTypeDeclaration @class = Define.Class("Configuration");

            @class.AddField(new CodeTypeReference(sectionClass.Name), MemberAttributes.Private | MemberAttributes.Static, "_config");
            @class.AddField(typeof(bool), MemberAttributes.Private | MemberAttributes.Static, "_configRead");

            @class.AddCtor(
                Emit.assignField("_configRead", () => false));
            return @class;


            //Figure out class name and output
            //string configClassName = options.ConfigClassName;
            //if (string.IsNullOrWhiteSpace(configClassName))
            //{
            //    configClassName = CreatePascalCaseIdentifier(configNode.Name,
            //        configNode.Name.EndsWith("Configuration") ? null : "Configuration");
            //}
            //var configClass = new CodeTypeDeclaration(configClassName) {
            //    IsPartial = true,
            //    TypeAttributes = TypeAttributes.Public | TypeAttributes.Sealed
            //};

            ////Output fields
            //var configField = new CodeMemberField(new CodeTypeReference(sectionClass.Name), "_config") {
            //    Attributes = MemberAttributes.Private | MemberAttributes.Static
            //};
            //configClass.Members.Add(configField);
            //var configReadfield = new CodeMemberField(typeof(bool), "_configRead") {
            //    Attributes = MemberAttributes.Private | MemberAttributes.Static
            //};
            //configClass.Members.Add(configReadfield);

            ////Output private ctor to simulate static class
            //var privateCtor = new CodeConstructor {
            //    Attributes = MemberAttributes.Private
            //};
            //configClass.Members.Add(privateCtor);

            ////Output static ctor
            //var staticCtor = new CodeTypeConstructor();
            //staticCtor.Statements.Add(new CodeAssignStatement(new CodeFieldReferenceExpression(null, configField.Name),
            //    new CodeCastExpression(new CodeTypeReference(sectionClass.Name),
            //        new CodeMethodInvokeExpression(
            //            new CodeTypeReferenceExpression(new CodeTypeReference(typeof(ConfigurationManager),
            //                CodeTypeReferenceOptions.GlobalReference)), "GetSection", new CodePrimitiveExpression(configNode.Name)))));
            //configClass.Members.Add(staticCtor);

            //var property = new CodeMemberProperty {
            //    Name = string.IsNullOrEmpty(options.ConfigPropertyName) ? "Current" : options.ConfigPropertyName,
            //    Attributes = MemberAttributes.Public | MemberAttributes.Static,
            //    Type = new CodeTypeReference(sectionClass.Name),
            //    HasGet = true
            //};
            //property.GetStatements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(null, configField.Name)));
            //configClass.Members.Add(property);

            ////Output partial methods
            //var validationMethod = new CodeMemberField("partial void", "ValidateConfiguration()");
            //validationMethod.Attributes = MemberAttributes.Static;
            //configClass.Members.Add(validationMethod);

            //return configClass;
        }

        private static CodeTypeDeclaration CreateElementClass(ConfigNode configNode, Type baseType, string classSuffix)
        {
            bool isCollection = configNode is CollectionConfigNode;

            if (baseType == null)
            {
                baseType = isCollection ? typeof(ConfigurationElementCollection) : typeof(ConfigurationElement);
                classSuffix = isCollection ? "ElementCollection" : "Element";
            }

            string sectionClassName = CreatePascalCaseIdentifier(configNode.Name, classSuffix);
            var sectionClass = new CodeTypeDeclaration(sectionClassName) {
                TypeAttributes = TypeAttributes.Public | TypeAttributes.Sealed,
                IsPartial = true
            };
            sectionClass.BaseTypes.Add(baseType);

            //Add attribute properties, which apply to both elements and collections
            foreach (ConfigNodeAttribute attribute in configNode.Attributes)
            {
                var property = new CodeMemberProperty {
                    Name = CreatePascalCaseIdentifier(attribute.Name, null),
                    Type = new CodeTypeReference(GetTypeFromAttributeType(attribute.Type)),
                    Attributes = MemberAttributes.Public | MemberAttributes.Final,
                    HasGet = true
                };

                property.GetStatements.Add(
                    new CodeMethodReturnStatement(new CodeCastExpression(property.Type.BaseType,
                        new CodeIndexerExpression(new CodeThisReferenceExpression(), new CodePrimitiveExpression(attribute.Name)))));
                property.HasSet = true;
                property.SetStatements.Add(
                    new CodeAssignStatement(
                        new CodeIndexerExpression(new CodeThisReferenceExpression(), new CodePrimitiveExpression(attribute.Name)),
                        new CodePropertySetValueReferenceExpression()));

                var customAttribute = new CodeAttributeDeclaration(new CodeTypeReference(typeof(ConfigurationPropertyAttribute)));
                customAttribute.Arguments.Add(new CodeAttributeArgument(new CodePrimitiveExpression(attribute.Name)));
                customAttribute.Arguments.Add(new CodeAttributeArgument("IsRequired", new CodePrimitiveExpression(!attribute.Optional)));
                property.CustomAttributes.Add(customAttribute);

                sectionClass.Members.Add(property);
            }

            //If the current node is a collection, add the necessary overrides
            if (isCollection)
            {
                var collectionNode = (CollectionConfigNode)configNode;

                CodeTypeDeclaration collectionItemClass = CreateElementClass(collectionNode.Child, null, null);
                sectionClass.Members.Add(collectionItemClass);

                var method = new CodeMemberMethod {
                    Name = "CreateNewElement",
                    ReturnType = new CodeTypeReference(typeof(ConfigurationElement)),
                    Attributes = MemberAttributes.Family | MemberAttributes.Override
                };
                method.Statements.Add(new CodeMethodReturnStatement(new CodeObjectCreateExpression(collectionItemClass.Name)));
                sectionClass.Members.Add(method);

                method = new CodeMemberMethod {
                    Name = "GetElementKey",
                    ReturnType = new CodeTypeReference(typeof(object)),
                    Attributes = MemberAttributes.Family | MemberAttributes.Override
                };
                method.Parameters.Add(new CodeParameterDeclarationExpression(typeof(ConfigurationElement), "element"));
                method.Statements.Add(
                    new CodeMethodReturnStatement(
                        new CodePropertyReferenceExpression(
                            new CodeCastExpression(collectionItemClass.Name, new CodeVariableReferenceExpression("element")),
                            CreatePascalCaseIdentifier(collectionNode.Child.Attributes.KeyName, null))));
                sectionClass.Members.Add(method);

                var indexer = new CodeMemberProperty {
                    Name = "Item",
                    Attributes = MemberAttributes.Public | MemberAttributes.Final,
                    Type = new CodeTypeReference(collectionItemClass.Name),
                    HasGet = true
                };
                indexer.GetStatements.Add(
                    new CodeMethodReturnStatement(new CodeCastExpression(collectionItemClass.Name,
                        new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), "BaseGet",
                            new CodeVariableReferenceExpression("i")))));
                indexer.Parameters.Add(new CodeParameterDeclarationExpression(new CodeTypeReference(typeof(int)), "i"));
                sectionClass.Members.Add(indexer);
            }

            //If the current node is not a collection, then we add a property for each child node
            if (!isCollection)
            {
                foreach (ConfigNode childNode in configNode.AllChildren)
                {
                    CodeTypeDeclaration childElementClass = CreateElementClass(childNode, null, null);
                    sectionClass.Members.Add(childElementClass);

                    var property = new CodeMemberProperty {
                        Name = CreatePascalCaseIdentifier(childNode.Name, null),
                        Type = new CodeTypeReference(childElementClass.Name),
                        Attributes = MemberAttributes.Public | MemberAttributes.Final,
                        HasGet = true
                    };
                    property.GetStatements.Add(
                        new CodeMethodReturnStatement(new CodeCastExpression(childElementClass.Name,
                            new CodeIndexerExpression(new CodeThisReferenceExpression(), new CodePrimitiveExpression(childNode.Name)))));

                    var propertyAttribute = new CodeAttributeDeclaration(new CodeTypeReference(typeof(ConfigurationPropertyAttribute)));
                    propertyAttribute.Arguments.Add(new CodeAttributeArgument(new CodePrimitiveExpression(childNode.Name)));
                    property.CustomAttributes.Add(propertyAttribute);

                    var childCollectionNode = childNode as CollectionConfigNode;
                    if (childCollectionNode != null)
                    {
                        string collectionItemName = childCollectionNode.Child.Name;
                        string collectionItemTypeName = CreatePascalCaseIdentifier(collectionItemName, "Element");
                        var collectionAttribute =
                            new CodeAttributeDeclaration(new CodeTypeReference(typeof(ConfigurationCollectionAttribute)));
                        collectionAttribute.Arguments.Add(
                            new CodeAttributeArgument(new CodeTypeOfExpression(childElementClass.Name + "." + collectionItemTypeName)));
                        collectionAttribute.Arguments.Add(new CodeAttributeArgument("AddItemName",
                            new CodePrimitiveExpression(collectionItemName)));
                        property.CustomAttributes.Add(collectionAttribute);
                    }

                    sectionClass.Members.Add(property);
                }
            }

            return sectionClass;
        }

        //Creates a Pascal-cased identifier from the given string. Does this by removing separator
        //characters like space, underscores, dashes, etc. and uppercasing the character following
        //the separator character.
        private static string CreatePascalCaseIdentifier(string str, string suffix)
        {
            var sb = new StringBuilder(str.Length);

            bool isAtSeparator = true;
            foreach (char ch in str)
            {
                //If the current character is a separator, set the flag and ignore the character
                if (!char.IsLetterOrDigit(ch))
                    isAtSeparator = true;
                    //If flag set, uppercase current character and set flag to false
                else if (isAtSeparator)
                {
                    sb.Append(char.ToUpper(ch));
                    isAtSeparator = false;
                } else
                    sb.Append(ch);
            }

            if (!string.IsNullOrEmpty(suffix))
                sb.Append(suffix);

            return sb.ToString();
        }

        private static Type GetTypeFromAttributeType(AttributeType attributeType)
        {
            switch (attributeType)
            {
                case AttributeType.Integer:
                    return typeof(long);
                case AttributeType.Floating:
                    return typeof(double);
                case AttributeType.Boolean:
                    return typeof(bool);
                case AttributeType.DateTime:
                    return typeof(DateTime);
                case AttributeType.Type:
                    return typeof(Type);
                default:
                    return typeof(string);
            }
        }
    }
}